﻿using System;
using System.Collections.Generic;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Text;
using System.Threading;

namespace Sharp.Interprocess.MemoryManager
{
    internal class SegmentAllocator
    {
        #region Private Members
        private const int MUTEX_TIMEOUT = 2000;
        private Mutex m_SyncObject;
        #endregion

        #region Constructor(s)
        public SegmentAllocator(Mutex writeMutex)
        {
            m_SyncObject = writeMutex;            
        }
        #endregion

        #region ISegmentAllocator Implementation
        public bool CanAllocate(int allocationSize, Dictionary<int, int> availableBlockCountBySize)
        {
            try
            {
                m_SyncObject.WaitOneWithException(MUTEX_TIMEOUT);

                var powerOf2AllocationSize = AllocatorUtil.FindClosestSegmentSize(allocationSize);
                return CanExactlyAllocate(powerOf2AllocationSize,availableBlockCountBySize) ||
                       CanAllocateAfterSplit(powerOf2AllocationSize,availableBlockCountBySize) ||
                       CanAllocateAfterPartition(powerOf2AllocationSize,availableBlockCountBySize);
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }
        }

        public IEnumerable<SegmentBlock> Allocate(int allocationSize, 
                                                  GeneralTree<SegmentBlock> segmentBlockData, 
                                                  Dictionary<int, int> availableBlockCountBySize,
                                                  HashSet<long> allocatedSegmentsBySizeAndOffset)
        {
            try
            {
                m_SyncObject.WaitOneWithException(MUTEX_TIMEOUT);
                IEnumerable<SegmentBlock> allocatedBlocks = null;
                var powerOf2AllocationSize = AllocatorUtil.FindClosestSegmentSize(allocationSize);

                if (CanExactlyAllocate(powerOf2AllocationSize, availableBlockCountBySize))
                {
                    allocatedBlocks = AllocateExactSize(powerOf2AllocationSize, segmentBlockData, availableBlockCountBySize, allocatedSegmentsBySizeAndOffset);
                }
                else if (CanAllocateAfterSplit(powerOf2AllocationSize,availableBlockCountBySize))
                {
                    allocatedBlocks = AllocateWithSplit(powerOf2AllocationSize, segmentBlockData, availableBlockCountBySize, allocatedSegmentsBySizeAndOffset);
                }
                else if (CanAllocateAfterPartition(powerOf2AllocationSize,availableBlockCountBySize))
                {
                    allocatedBlocks = AllocateWithPartition(powerOf2AllocationSize, segmentBlockData, availableBlockCountBySize, allocatedSegmentsBySizeAndOffset);
                }
                
                if(allocatedBlocks == null)
                {
                    throw new OutOfMemoryException("Not enough memory to allocate required segment");
                }

                return allocatedBlocks;
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }            
        }

        #endregion

        #region Helper Methods

        private IEnumerable<SegmentBlock> AllocateWithPartition(int powerOf2AllocationSize, 
                                                                GeneralTree<SegmentBlock> segmentBlockData, 
                                                                Dictionary<int, int> availableBlockCountBySize,
                                                                HashSet<long> allocatedSegmentsBySizeAndOffset)
        {
            var blockSizeToMerge = availableBlockCountBySize.First(row => row.Key * row.Value >= powerOf2AllocationSize).Key;

            var neededSegmentBlockCount = powerOf2AllocationSize / blockSizeToMerge;
            IEnumerable<GeneralTree<SegmentBlock>> foundSegmentBlocks;
            if (segmentBlockData.TryFindMultipleNodes(segmentBlock => segmentBlock.BlockSize == blockSizeToMerge &&
                                                                      !allocatedSegmentsBySizeAndOffset.Contains(segmentBlock.BlockOffset),  
                                                                      neededSegmentBlockCount,
                                                                      out foundSegmentBlocks))
            {
                foreach (var segmentNode in foundSegmentBlocks)
                {
                    MarkAllocated(segmentNode,availableBlockCountBySize,allocatedSegmentsBySizeAndOffset);
                }                

                return foundSegmentBlocks.Select(segmentNode => segmentNode.Data).ToList();
            }
            else
            {
                return null;
            }
            
        }

        private IEnumerable<SegmentBlock> AllocateWithSplit(int allocationSize,
                                                            GeneralTree<SegmentBlock> segmentBlockData,
                                                            Dictionary<int, int> availableBlockCountBySize,
                                                            HashSet<long> allocatedSegmentsBySizeAndOffset)
        {
            var relevantNode = segmentBlockData.FindNodeAndSplit(allocationSize,allocatedSegmentsBySizeAndOffset);
            if (relevantNode == null)
            {
                throw new ApplicationException(String.Format("Could not find node with specified block size (block size = {0}", allocationSize));
            }

            MarkAllocated(relevantNode,availableBlockCountBySize,allocatedSegmentsBySizeAndOffset);            

            return new List<SegmentBlock>() { relevantNode.Data };
        }


        private IEnumerable<SegmentBlock> AllocateExactSize(int allocationSize,
                                                            GeneralTree<SegmentBlock> segmentBlockData, 
                                                            Dictionary<int, int> availableBlockCountBySize,
                                                            HashSet<long> allocatedSegmentsBySizeAndOffset)
        {
            var relevantNode = segmentBlockData.FindNode(node => node.BlockSize == allocationSize &&
                                                                 !allocatedSegmentsBySizeAndOffset.Contains(node.BlockOffset));
            if (relevantNode == null)
            {
                throw new ApplicationException(String.Format("Could not find node with specified block size (block size = {0}",allocationSize));
            }

            MarkAllocated(relevantNode, availableBlockCountBySize,allocatedSegmentsBySizeAndOffset);
            
            return new List<SegmentBlock>(){ relevantNode.Data };
        }

        private void MarkAllocated(GeneralTree<SegmentBlock> segmentBlockData, 
                                   Dictionary<int, int> availableBlockCountBySize, 
                                   HashSet<long> allocatedSegmentsBySizeAndOffset)
        {
            if (!allocatedSegmentsBySizeAndOffset.Contains(segmentBlockData.Data.BlockOffset))
            {
                allocatedSegmentsBySizeAndOffset.Add(segmentBlockData.Data.BlockOffset);
            }

            segmentBlockData.Ancestors
                            .ToList()
                            .ForEach(segmentBlockNode =>
                                {
                                    if (availableBlockCountBySize[segmentBlockNode.Data.BlockSize] > 0)
                                    {
                                        availableBlockCountBySize[segmentBlockNode.Data.BlockSize]--;
                                    }                                    
                                });

            MarkChildrenAllocated(segmentBlockData,availableBlockCountBySize,allocatedSegmentsBySizeAndOffset);
        }

        private void MarkChildrenAllocated(GeneralTree<SegmentBlock> segmentBlockData,
                                           Dictionary<int, int> availableBlockCountBySize, 
                                           HashSet<long> allocatedSegmentsBySizeAndOffset)
        {
            var allocationSegmentBlockData = Tuple.Create(segmentBlockData.Data.BlockSize, segmentBlockData.Data.BlockOffset);
            if (!allocatedSegmentsBySizeAndOffset.Contains(segmentBlockData.Data.BlockOffset))
            {
                allocatedSegmentsBySizeAndOffset.Add(segmentBlockData.Data.BlockOffset);
            }

            if (availableBlockCountBySize[segmentBlockData.Data.BlockSize] > 0)
            {
                availableBlockCountBySize[segmentBlockData.Data.BlockSize]--;
            }

            if (segmentBlockData.ChildNodes != null)
            {
                segmentBlockData.ChildNodes.Where(node => !allocatedSegmentsBySizeAndOffset.Contains(node.Data.BlockOffset))
                                           .ToList()
                                           .ForEach(node => MarkChildrenAllocated(node, availableBlockCountBySize, allocatedSegmentsBySizeAndOffset));
            }
        }

        private bool CanAllocateAfterSplit(int powerOf2AllocationSize, Dictionary<int, int> availableBlockCountBySize)
        {
            return availableBlockCountBySize.Any(kvp => kvp.Key >= powerOf2AllocationSize && kvp.Value > 0);
        }

        private bool CanExactlyAllocate(int powerOf2AllocationSize, Dictionary<int, int> availableBlockCountBySize)
        {
            return availableBlockCountBySize.ContainsKey(powerOf2AllocationSize) &&
                   availableBlockCountBySize[powerOf2AllocationSize] > 0;
        }

        private bool CanAllocateAfterPartition(int powerOf2AllocationSize,Dictionary<int,int> availableBlockCountBySize)
        {
            return availableBlockCountBySize.Any(row => (row.Key * row.Value) >= powerOf2AllocationSize);
        }

        #endregion
    }
}
